#!/usr/bin/env ruby

# frozen_string_literal: true

require 'open3'
require 'fileutils'
require 'uri'

class SchemaRegenerator
  ##
  # Filename of the schema
  #
  # This file is being regenerated by this script.
  FILENAME = 'db/structure.sql'

  ##
  # Directories where migrations are stored
  #
  # The methods +hide_migrations+ and +unhide_migrations+ will rename
  # these to disable/enable migrations.
  MIGRATION_DIRS = %w[db/migrate db/post_migrate].freeze

  ##
  # Directory where we store schema versions
  #
  # The remove_schema_migration_files removes files added in this
  # directory when it runs.
  SCHEMA_MIGRATIONS_DIR = 'db/schema_migrations/'

  def execute
    Dir.chdir(File.expand_path('..', __dir__)) do
      checkout_ref
      checkout_clean_schema
      hide_migrations
      remove_schema_migration_files
      stop_spring
      reset_db
      unhide_migrations
      migrate
    ensure
      unhide_migrations
    end
  end

  private

  ##
  # Git checkout +CI_COMMIT_SHA+.
  #
  # When running from CI, checkout the clean commit,
  # not the merged result.
  def checkout_ref
    return unless ci?

    run %[git checkout #{source_ref}]
    run %q[git clean -f -- db]
  end

  ##
  # Checkout the clean schema from the target branch
  def checkout_clean_schema
    remote_checkout_clean_schema || local_checkout_clean_schema
  end

  ##
  # Get clean schema from remote servers
  #
  # This script might run in CI, using a shallow clone, so to checkout
  # the file, fetch the target branch from the server.
  def remote_checkout_clean_schema
    return false unless project_url
    return false unless target_project_url

    run %[git remote add target_project #{target_project_url}.git]
    run %[git fetch target_project #{target_branch}:#{target_branch}]

    local_checkout_clean_schema
  end

  ##
  # Git checkout the schema from target branch.
  #
  # Ask git to checkout the schema from the target branch and reset
  # the file to unstage the changes.
  def local_checkout_clean_schema
    run %[git checkout #{merge_base} -- #{FILENAME}]
    run %[git reset -- #{FILENAME}]
  end

  ##
  # Move migrations to where Rails will not find them.
  #
  # To reset the database to clean schema defined in +FILENAME+, move
  # the migrations to a path where Rails will not find them, otherwise
  # +db:reset+ would abort. Later when the migrations should be
  # applied, use +unhide_migrations+ to bring them back.
  def hide_migrations
    MIGRATION_DIRS.each do |dir|
      File.rename(dir, "#{dir}__")
    end
  end

  ##
  # Undo the effect of +hide_migrations+.
  #
  # Place back the migrations which might be moved by
  # +hide_migrations+.
  def unhide_migrations
    error = nil

    MIGRATION_DIRS.each do |dir|
      File.rename("#{dir}__", dir)
    rescue Errno::ENOENT
      nil
    rescue StandardError => e
      # Save error for later, but continue with other dirs first
      error = e
    end

    raise error if error
  end

  ##
  # Remove files added to db/schema_migrations
  #
  # In order to properly reset the database and re-run migrations
  # the schema migrations for new migrations must be removed.
  def remove_schema_migration_files
    (untracked_schema_migrations + committed_schema_migrations).each do |schema_migration|
      FileUtils.rm(schema_migration)
    end
  end

  ##
  # List of untracked schema migrations
  #
  # Get a list of schema migrations that are not tracked so we can remove them
  def untracked_schema_migrations
    git_command = "git ls-files --others --exclude-standard -- #{SCHEMA_MIGRATIONS_DIR}"
    run(git_command).chomp.split("\n")
  end

  ##
  # List of untracked schema migrations
  #
  # Get a list of schema migrations that have been committed since the last
  def committed_schema_migrations
    git_command = "git diff --name-only --diff-filter=A #{merge_base} -- #{SCHEMA_MIGRATIONS_DIR}"
    run(git_command).chomp.split("\n")
  end

  ##
  # Stop spring before modifying the database
  def stop_spring
    run %q[bin/spring stop]
  end

  ##
  # Run rake task to reset the database.
  def reset_db
    run %q[bin/rails db:reset RAILS_ENV=test]
  end

  ##
  # Run rake task to run migrations.
  def migrate
    run %q[bin/rails db:migrate RAILS_ENV=test]
  end

  ##
  # Run the given +cmd+.
  #
  # The command is colored green, and the output of the command is
  # colored gray.
  # When the command failed an exception is raised.
  def run(cmd)
    puts "\e[32m$ #{cmd}\e[37m"
    stdout_str, stderr_str, status = Open3.capture3(cmd)
    puts "#{stdout_str}#{stderr_str}\e[0m"
    raise("Command failed: #{stderr_str}") unless status.success?

    stdout_str
  end

  ##
  # Return the base commit between source and target branch.
  def merge_base
    @merge_base ||= run("git merge-base #{target_branch} #{source_ref}").chomp
  end

  ##
  # Return the name of the target branch
  #
  # Get source ref from CI environment variable, or read the +TARGET+
  # environment+ variable, or default to +HEAD+.
  def target_branch
    ENV['CI_MERGE_REQUEST_TARGET_BRANCH_NAME'] || ENV['TARGET'] || ENV['CI_DEFAULT_BRANCH'] || 'master'
  end

  ##
  # Return the source ref
  #
  # Get source ref from CI environment variable, or default to +HEAD+.
  def source_ref
    ENV['CI_COMMIT_SHA'] || 'HEAD'
  end

  ##
  # Return the source project URL from CI environment variable.
  def project_url
    ENV['CI_PROJECT_URL']
  end

  ##
  # Return the target project URL from CI environment variable.
  def target_project_url
    ENV['CI_MERGE_REQUEST_PROJECT_URL']
  end

  ##
  # Return whether the script is running from CI
  def ci?
    ENV['CI']
  end
end

SchemaRegenerator.new.execute
